﻿using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using ProxyServer.Entities;
using ProxyServer.EntityFrameworkCore;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Threading.Tasks;
using Yarp.ReverseProxy.Configuration;

namespace ProxyServer.Yarp;

public sealed class EventIds
{
    public static readonly EventId ErrorSignalingChange = new(1, nameof(ErrorSignalingChange));
    public static readonly EventId LoadData = new(2, nameof(LoadData));
    public static readonly EventId ConfigurationDataConversionFailed = new(3, nameof(ConfigurationDataConversionFailed));
}

public class AppProxyConfigProvider : IProxyConfigProvider, IDisposable
{

    private static class Log
    {
        private static readonly Action<ILogger, Exception> _errorSignalingChange = LoggerMessage.Define(LogLevel.Error, EventIds.ErrorSignalingChange, "An exception was thrown from the change notification.");

        private static readonly Action<ILogger, Exception?> _loadData = LoggerMessage.Define(LogLevel.Information, EventIds.LoadData, "Loading proxy data from config.");

        private static readonly Action<ILogger, Exception> _configurationDataConversionFailed = LoggerMessage.Define(LogLevel.Error, EventIds.ConfigurationDataConversionFailed, "Configuration data conversion failed.");

        public static void ErrorSignalingChange(ILogger logger, Exception exception)
        {
            _errorSignalingChange(logger, exception);
        }

        public static void LoadData(ILogger logger)
        {
            _loadData(logger, null);
        }

        public static void ConfigurationDataConversionFailed(ILogger logger, Exception exception)
        {
            _configurationDataConversionFailed(logger, exception);
        }
    }

    private readonly ILogger _logger;
    private readonly IServiceProvider serviceProvider;
    private readonly object _lockObject = new object();

    private CancellationTokenSource? _changeToken;
    private bool _disposed;
    private IDisposable? _subscription;
    private AppProxyConfig? _snapshot;
    private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
    private readonly IMediator _mediator;

    public AppProxyConfigProvider(ILogger<AppProxyConfigProvider> logger, IServiceProvider serviceProvider, IMediator mediator)
    {
        this._mediator = mediator;
        this._logger = logger;
        this.serviceProvider = serviceProvider;
    }


    private void RaiseChanged()
    {
        var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
        previousToken.OnReload();
    }


    public void Reload() => RaiseChanged();

    public IChangeToken GetReloadToken() => _reloadToken;

    private (List<RouteConfig>, List<ClusterConfig>) LoadFromServices()
    {
        var clusters = new List<ClusterConfig>();
        var routes = new List<RouteConfig>();

        using var scope = serviceProvider.CreateAsyncScope();
        using var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var services = db.Set<ServiceEntity>()
            .AsNoTracking()
            .ToList();


        foreach (var service in services)
        {
            var route = new RouteConfig()
            {
                RouteId = service.Id.ToString(),
                ClusterId = service.Name.ToString(),
                Match = new RouteMatch
                {
                    Methods = service.Methods,
                    Hosts = service.Hosts,
                    Path = service.MatchPath
                },
                Metadata = new Dictionary<string, string>
                        {
                            { "ServiceName", service.Name }
                        }
            };

            if (string.IsNullOrWhiteSpace(service.RouteOverwrite) == false)
            {
                var overwrite = JsonSerializer.Deserialize<RouteConfig>(service.RouteOverwrite);
                if (overwrite is not null)
                {
                    route = route with
                    {
                        RouteId = service.Id.ToString(),
                        ClusterId = service.Name.ToString(),
                        Match = new RouteMatch
                        {
                            Methods = service.Methods,
                            Hosts = service.Hosts,
                            Path = service.MatchPath
                        },
                        Metadata = new Dictionary<string, string>
                        {
                            { "ServiceName", service.Name }
                        }
                    };
                }
            }


            routes.Add(route);

            var cluster = new ClusterConfig()
            {
                ClusterId = service.Name.ToString(),
                Destinations = service.Endpoints.ToDictionary(
                    e => e.Name.ToString(),
                    e => new DestinationConfig
                    {
                        Address = e.Address
                    })
            };

            clusters.Add(cluster);
        }

        return (routes, clusters);
    }

    public void Apply(Guid id, List<RouteConfig> routes, List<ClusterConfig> clusters)
    {
        lock (_lockObject)
        {
            ReplaceConfig(id, routes, clusters);
        }
    }

    private (Guid id, List<RouteConfig> routes, List<ClusterConfig> clusters) LoadLastSnapshot()
    {
        Log.LoadData(_logger);

        try
        {
            using var scope = serviceProvider.CreateAsyncScope();
            using var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();

            var snapshot = db.Snapshots
                .AsNoTracking()
                .Where(x => x.RemovedAt == null)
                .OrderByDescending(x => x.AppliedAt)
                .FirstOrDefault();

            if (snapshot is null)
            {
                return (Guid.NewGuid(), new List<RouteConfig>(), new List<ClusterConfig>());
            }

            return (snapshot.Id, snapshot.Routes, snapshot.Clusters);
        }
        catch (Exception ex)
        {
            Log.ConfigurationDataConversionFailed(_logger, ex);
            throw;
        }
    }

    [MemberNotNull("_snapshot")]
    public void InitConfig()
    {
        lock (_lockObject)
        {
            if (_snapshot is not null)
            {
                return;
            }

            var (id, routes, clusters) = LoadLastSnapshot();

            //_changeToken = new CancellationTokenSource();
            //_snapshot = new AppProxyConfig(routes, clusters, new CancellationChangeToken(_changeToken.Token))
            //{
            //    SnapshotId = id == Guid.Empty ? Guid.NewGuid() : id,
            //};

            ReplaceConfig(id, routes, clusters);
        }
    }

    [MemberNotNull("_snapshot")]
    private void ReplaceConfig(Guid id, List<RouteConfig> routes, List<ClusterConfig> clusters)
    {
        var _old = _snapshot;
        var changeToken = _changeToken;
        _changeToken = new CancellationTokenSource();

        var snapshot = new AppProxyConfig(routes, clusters, new CancellationChangeToken(_changeToken.Token)) { SnapshotId = id };
        _snapshot = snapshot;

        try
        {
            changeToken?.Cancel(throwOnFirstException: false);

            // publish and get snapshot id;
            _mediator.Publish(new ProxyConfigAppliedEvent(_snapshot)).ConfigureAwait(false);
            if (_old is not null)
            {
                _mediator.Publish(new ProxyConfigRemovedEevet(_old)).ConfigureAwait(false);
            }
        }
        catch (Exception ex)
        {
            Log.ErrorSignalingChange(_logger, ex);
        }
    }

    [MemberNotNull("_snapshot")]
    public void UpdateConfig()
    {
        lock (_lockObject)
        {
            var (routes, clusters) = LoadFromServices();

            ReplaceConfig(Guid.NewGuid(), routes, clusters);
        }
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // TODO: 释放托管状态(托管对象)
            }

            // TODO: 释放未托管的资源(未托管的对象)并重写终结器
            // TODO: 将大型字段设置为 null
            _subscription?.Dispose();
            _changeToken?.Dispose();
            _disposed = true;
        }
    }

    // // TODO: 仅当“Dispose(bool disposing)”拥有用于释放未托管资源的代码时才替代终结器
    // ~AppProxyConfigProvider()
    // {
    //     // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
    //     Dispose(disposing: false);
    // }

    public void Dispose()
    {
        // 不要更改此代码。请将清理代码放入“Dispose(bool disposing)”方法中
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public IProxyConfig GetConfig()
    {
        if (_snapshot == null)
        {
            _subscription = ChangeToken.OnChange(GetReloadToken, UpdateConfig);
            InitConfig();
        }
        return _snapshot;
    }
}
